// This file is subject to a 1-clause BSD license.
// Its contents can be found in the enclosed LICENSE file.

package owm

import (
	"encoding/json"
	"fmt"
	"log"
	"math"
	"net/url"
	"strings"
	"time"

	"notabug.org/mouz/bot/app/util"
	"notabug.org/mouz/bot/irc"
	"notabug.org/mouz/bot/irc/cmd"
	"notabug.org/mouz/bot/irc/proto"
)

const currentWeatherURL = "https://api.openweathermap.org/data/2.5/weather?" +
	"units=%s&mode=json&lang=%s&appid=%s&q=%s"

// cmdCurrentWeather yields current weather data for a given location.
func (p *plugin) cmdCurrentWeather(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
	p.m.Lock()
	defer p.m.Unlock()

	if len(p.config.OwmAPIKey) == 0 {
		log.Println("[owm] API key not in configuration")
		_ = proto.PrivMsg(w, r.Target, TextNoWeather, r.SenderName)
		return
	}

	loc := getLocation(r)

	if cached, ok := p.currentWeatherCache[loc]; ok {
		// If the cached result is younger than the timeout, print its
		// contents for the user and exit. Otherwise, consider it stale,
		// delete it and re-fetch.
		if time.Since(cached.Timestamp) <= CacheTimeout {
			log.Println("[owm] using cached item for current weather")
			err := sendCurrentWeather(w, r, cached)
			if err != nil {
				log.Println("[owm]", err)
			}
			return
		}
		delete(p.currentWeatherCache, loc)
	}

	// Fetch new response.
	data, err := p.fetch(currentWeatherURL, loc)
	if err != nil {
		log.Println("[owm]", err)
		return
	}

	err = p.sendResult(w, r, data)
	if err != nil {
		log.Println("[owm]", err)
		return
	}

}

// sendResult sends the result back to the user. The result is either
// an error message or a meaningful current weather report.
func (p *plugin) sendResult(w irc.ResponseWriter, r *irc.Request, data []byte) error {

	// unmarshal into a generic map, just to be able to detect the kind
	// of data that came in from the weather server
	var js map[string]interface{}
	err := json.Unmarshal(data, &js)
	if err != nil {
		return err
	}

	if len(js) == 2 {
		// assume error message from weather server
		var oerr owmError
		err := json.Unmarshal(data, &oerr)
		if err != nil {
			return err
		}

		return sendError(w, r, &oerr)
	}

	// assume meaningful current weather response
	var owmcur owmCurrent
	owmcur.Timestamp = time.Now()
	err = json.Unmarshal(data, &owmcur)
	if err != nil {
		return err
	}
	loc := getLocation(r)
	p.currentWeatherCache[loc] = &owmcur
	return sendCurrentWeather(w, r, &owmcur)

}

// getLocation extracts the requested location
func getLocation(r *irc.Request) string {
	l := ""
	for _, s := range r.Fields(1) {
		l += url.QueryEscape(strings.ToLower(s)) + " "
	}
	return strings.TrimSuffix(l, " ")
}

// sendError reports on an error message coming from
// openweathermap.org back to the user
func sendError(w irc.ResponseWriter, r *irc.Request, oErr *owmError) error {
	loc, _ := url.QueryUnescape(getLocation(r))

	str, ok := oErr.Code.(string)
	if ok && str == "404" {
		return proto.PrivMsg(w, r.Target, TextNotFound404, util.Bold(loc))
	} else {
		log.Printf("[owm] weather server returned error %+v %s [%s]\n", oErr.Code, oErr.Message, loc)
	}

	return nil
}

// sendCurrentWeather formats the current weather and sends it back to
// the user
func sendCurrentWeather(w irc.ResponseWriter, r *irc.Request, owmcur *owmCurrent) error {
	location := util.Bold(owmcur.Name)
	location += fmt.Sprintf(" (%s)", owmcur.Sys.Country)

	temp := fmt.Sprintf(TextTempExact, owmcur.Main.Temp)
	if owmcur.Main.TempMax-owmcur.Main.TempMin > 1 {
		temp = fmt.Sprintf(TextTempRange, owmcur.Main.TempMin, owmcur.Main.TempMax)
	}

	precip := ""
	if owmcur.Snow.OneH > 0 {
		precip = fmt.Sprintf(TextSnowFall, owmcur.Snow.OneH)
	} else if owmcur.Snow.ThreeH > 0 {
		precip = fmt.Sprintf(TextSnowFall, owmcur.Snow.ThreeH/3)
	} else if owmcur.Rain.OneH > 0 {
		precip = fmt.Sprintf(TextRainFall, owmcur.Rain.OneH)
	} else if owmcur.Rain.ThreeH > 0 {
		precip = fmt.Sprintf(TextRainFall, owmcur.Rain.ThreeH/3)
	}

	index := int(0.5+owmcur.Wind.Deg/(360/16)) % 16
	winddir := TextWindDirection[index]

	speedBf := math.Pow(float64(owmcur.Wind.Speed)/0.836, 0.666)

	loc, _ := time.LoadLocation(TextTimeZone)
	t := time.Unix(owmcur.Sys.Sunrise, 0).In(loc)
	sunrise := t.Format(TextTimeFormat)
	t = time.Unix(owmcur.Sys.Sunset, 0).In(loc)
	sunset := t.Format(TextTimeFormat)
	sunriseset := fmt.Sprintf(TextSunRiseSet, sunrise, sunset)

	return proto.PrivMsg(w, r.Target, TextCurrentWeatherDisplay,
		r.SenderName,
		location,
		temp,
		owmcur.Weather[0].Description,
		precip,
		owmcur.Main.Pressure,
		owmcur.Main.Humidity,
		owmcur.Wind.Speed,
		speedBf,
		winddir,
		sunriseset,
	)
}

// owmError defines the API response when the request for the
// current weather was not succesful.
type owmError struct {
	Code    interface{} `json:"cod"`
	Message string      `json:"message"`
}

// owmCurrent defines the API response when the request for the
// current weather was succesful.
type owmCurrent struct {
	Timestamp  time.Time
	Coord      coordinate         `json:"coord"`
	Weather    []weatherCondition `json:"weather"`
	Base       string             `json:"base"`
	Main       mainWeather        `json:"main"`
	Visibility int                `json:"visibility"`
	Wind       windData           `json:"wind"`
	Clouds     cloudiness         `json:"clouds"`
	Rain       rainVolume         `json:"rain"`
	Snow       snowVolume         `json:"snow"`
	Dt         int64              `json:"dt"`
	Sys        sysData            `json:"sys"`
	ID         int                `json:"id"`
	Name       string             `json:"name"`
	Cod        int                `json:"cod"`
}

type coordinate struct {
	Lon float32 `json:"lon"`
	Lat float32 `json:"lat"`
}

type weatherCondition struct {
	ID          int    `json:"id"`
	Main        string `json:"main"`
	Description string `json:"description"`
	Icon        string `json:"icon"`
}

type mainWeather struct {
	Temp        float32 `json:"temp"`
	Pressure    float32 `json:"pressure"`
	Humidity    float32 `json:"humidity"`
	TempMin     float32 `json:"temp_min"`
	TempMax     float32 `json:"temp_max"`
	SeaLevel    float32 `json:"sea_level"`
	GroundLevel float32 `json:"grnd_level"`
}

type windData struct {
	Speed float32 `json:"speed"`
	Deg   float32 `json:"deg"`
}

type cloudiness struct {
	All int `json:"all"`
}

type rainVolume struct {
	OneH   float32 `json:"1h"`
	ThreeH float32 `json:"3h"`
}

type snowVolume struct {
	OneH   float32 `json:"1h"`
	ThreeH float32 `json:"3h"`
}

type sysData struct {
	Type    int     `json:"type"`
	ID      int     `json:"id"`
	Message float32 `json:"message"`
	Country string  `json:"country"`
	Sunrise int64   `json:"sunrise"`
	Sunset  int64   `json:"sunset"`
}
